package com.greenzebras.geocache.datastore.persistence;

import java.util.ArrayList;
import java.util.List;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.location.Location;
import android.os.AsyncTask;
import android.util.Log;

import com.greenzebras.geocache.cachedata.Cache;
import com.greenzebras.geocache.cachedata.CacheCoordinate;
import com.greenzebras.geocache.cachedata.CacheType;
import com.greenzebras.geocache.datastore.GeocacheCacheDataManager;
import com.greenzebras.geocache.datastore.GeocacheCacheManagementRequestResponseListener;
import com.greenzebras.geocache.datastore.GeocacheCacheRequestResponseListener;

public enum GeocacheDataService implements GeocacheCacheDataManager {
	INSTANCE();

	private GeocacheDatabaseHelper geocacheDatabaseHelper = null;
	private static final String NO_WHERE_CLAUSE = null;
	private static final String DEFAULT_ORDER_BY = null;
	private static final long INSERT_FAILED = -1l;
	private static final String LIMIT_CLAUSE = "50";
	private static final float LOCAL_CACHE_RADIAL_DISTANCE_IN_METERS = 2000;

	public void initGeocacheDataService(Context context) {
		if (null == geocacheDatabaseHelper) {
			geocacheDatabaseHelper = new GeocacheDatabaseHelper(context);
		}
	}

	@Override
	public void getCachesForType(
			final GeocacheCacheRequestResponseListener geocacheCacheRequestResponseListener,
			final CacheType cacheType) {
		new AsyncTask<Void, Void, List<Cache>>() {
			@Override
			protected List<Cache> doInBackground(Void... params) {
				final StringBuilder typeWhereClauseBuilder = new StringBuilder(
						GeocacheDataServiceConstants.CACHE_COLUMN_TYPE).append(
						GeocacheDataServiceConstants.EQUALS).append(
						cacheType.toString());

				return executeCacheWhereQuery(
						typeWhereClauseBuilder.toString(), DEFAULT_ORDER_BY);
			}

			@Override
			protected void onPostExecute(List<Cache> result) {
				super.onPostExecute(result);
				geocacheCacheRequestResponseListener
						.processCachesRequestResponse(result);
			}

		}.execute();
	}

	@Override
	public void getCacheByName(
			final GeocacheCacheRequestResponseListener geocacheCacheRequestResponseListener,
			final String name) {
		new AsyncTask<Void, Void, List<Cache>>() {
			StringBuilder nameWhereClauseBuilder = new StringBuilder(
					GeocacheDataServiceConstants.CACHE_COLUMN_NAME).append(
					GeocacheDataServiceConstants.EQUALS).append(name);

			@Override
			protected List<Cache> doInBackground(Void... params) {
				return executeCacheWhereQuery(
						nameWhereClauseBuilder.toString(), DEFAULT_ORDER_BY);
			}

			@Override
			protected void onPostExecute(List<Cache> result) {
				super.onPostExecute(result);
				Cache cacheForCallback = result.iterator().hasNext() ? result
						.iterator().next() : null;

				geocacheCacheRequestResponseListener
						.processCacheRequestResponse(cacheForCallback);
			}
		}.execute();
	}

	@Override
	public void getCachesByNameSimilarTo(
			final GeocacheCacheRequestResponseListener geocacheCacheRequestResponseListener,
			final String name) {

		new AsyncTask<Void, Void, List<Cache>>() {
			@Override
			protected List<Cache> doInBackground(Void... params) {

				final StringBuilder nameSimilarWhereClauseBuilder = new StringBuilder(
						GeocacheDataServiceConstants.CACHE_COLUMN_NAME);
				nameSimilarWhereClauseBuilder.append(
						GeocacheDataServiceConstants.LIKE).append(
						quote(GeocacheDataServiceConstants.WILDCARD + name
								+ GeocacheDataServiceConstants.WILDCARD));

				return executeCacheWhereQuery(
						nameSimilarWhereClauseBuilder.toString(),
						DEFAULT_ORDER_BY);
			}

			@Override
			protected void onPostExecute(List<Cache> result) {
				super.onPostExecute(result);
				geocacheCacheRequestResponseListener
						.processCachesRequestResponse(result);
			}

		}.execute();
	}

	@Override
	public void getCachesForTypes(
			final GeocacheCacheRequestResponseListener geocacheCacheRequestResponseListener,
			final List<String> cacheTypes) {

		new AsyncTask<Void, Void, List<Cache>>() {
			@Override
			protected List<Cache> doInBackground(Void... params) {

				final StringBuilder typesFilterWhereClauseBuilder = new StringBuilder(
						GeocacheDataServiceConstants.CACHE_COLUMN_TYPE);
				typesFilterWhereClauseBuilder.append(
						GeocacheDataServiceConstants.IN).append(
						paren(implodeAndQuote(cacheTypes, ", ")));

				Log.i(getClass().getName(), "Query: "
						+ typesFilterWhereClauseBuilder.toString());
				return executeCacheWhereQuery(
						typesFilterWhereClauseBuilder.toString(),
						DEFAULT_ORDER_BY);
			}

			@Override
			protected void onPostExecute(List<Cache> result) {
				super.onPostExecute(result);
				geocacheCacheRequestResponseListener
						.processCachesRequestResponse(result);
			}

		}.execute();
	}

	@Override
	public void getCachesDistanceFrom(
			final GeocacheCacheRequestResponseListener geocacheCacheRequestResponseListener,
			final CacheCoordinate cacheCoordinate,
			final float radialDistanceInMeters) {
		new AsyncTask<Void, Void, List<Cache>>() {
			@Override
			protected List<Cache> doInBackground(Void... params) {
				Location argLocation = new Location("");
				Location currentCacheLocation = new Location("");

				setLocationLatLong(argLocation, cacheCoordinate);
				List<Cache> queryCaches = executeCacheWhereQuery(
						NO_WHERE_CLAUSE, DEFAULT_ORDER_BY);
				List<Cache> cachesWithinRadialDistance = new ArrayList<Cache>();
				for (Cache cache : queryCaches) {
					setLocationLatLong(currentCacheLocation,
							cache.getCacheCoordinate());
					if (Math.abs(argLocation.distanceTo(currentCacheLocation)) <= radialDistanceInMeters) {
						cachesWithinRadialDistance.add(cache);
					}
				}

				return cachesWithinRadialDistance;
			}

			@Override
			protected void onPostExecute(List<Cache> result) {
				super.onPostExecute(result);
				geocacheCacheRequestResponseListener
						.processCachesRequestResponse(result);
			}

		}.execute();
	}

	@Override
	public void getLocalCaches(
			final GeocacheCacheRequestResponseListener geocacheCacheRequestResponseListener,
			final CacheCoordinate cacheCoordinate) {
		getCachesDistanceFrom(geocacheCacheRequestResponseListener,
				cacheCoordinate, LOCAL_CACHE_RADIAL_DISTANCE_IN_METERS);
	}

	@Override
	public void createCache(
			final GeocacheCacheManagementRequestResponseListener geocacheCacheManagementRequestResponseListener,
			final Cache cache) {
		new AsyncTask<Void, Void, Cache>() {
			@Override
			protected Cache doInBackground(Void... params) {
				Boolean insertSucceeded = geocacheDatabaseHelper.executeInsert(
						GeocacheDataServiceConstants.CACHE_TABLE_NAME,
						buildCacheContentValuesForInsert(cache));

				if (insertSucceeded) {
					return cache;
				} else {
					return null;
				}
			}

			@Override
			protected void onPostExecute(Cache result) {
				super.onPostExecute(result);
				GeocacheCacheManagementRequestResponseListener.RequestStatus createRequestStatus = GeocacheCacheManagementRequestResponseListener.RequestStatus.SUCCESS;
				if (null == cache) {
					createRequestStatus = GeocacheCacheManagementRequestResponseListener.RequestStatus.FAIL;
				}

				geocacheCacheManagementRequestResponseListener
						.processGeocacheCacheManagementRequestResponse(
								createRequestStatus, cache);
			}
		}.execute();
	}

	@Override
	public void updateCache(
			final GeocacheCacheManagementRequestResponseListener geocacheCacheManagementRequestResponseListener,
			final Cache cache) {
		new AsyncTask<Void, Void, Cache>() {
			@Override
			protected Cache doInBackground(Void... params) {
				Boolean updateSucceeded = geocacheDatabaseHelper.executeUpdate(
						GeocacheDataServiceConstants.CACHE_TABLE_NAME,
						buildCacheContentValuesForInsert(cache),
						createCacheEqualityWhereClause(cache));

				if (updateSucceeded.booleanValue()) {
					return cache;
				} else {
					return null;
				}
			}

			@Override
			protected void onPostExecute(Cache result) {
				super.onPostExecute(result);
				GeocacheCacheManagementRequestResponseListener.RequestStatus updateRequestStatus = GeocacheCacheManagementRequestResponseListener.RequestStatus.SUCCESS;
				if (null == cache) {
					updateRequestStatus = GeocacheCacheManagementRequestResponseListener.RequestStatus.FAIL;
				}

				geocacheCacheManagementRequestResponseListener
						.processGeocacheCacheManagementRequestResponse(
								updateRequestStatus, cache);
			}
		}.execute();

	}

	@Override
	public void deleteCache(
			final GeocacheCacheManagementRequestResponseListener geocacheCacheManagementRequestResponseListener,
			final Cache cache) {
		new AsyncTask<Void, Void, Cache>() {
			@Override
			protected Cache doInBackground(Void... params) {
				Boolean deleteSucceeded = geocacheDatabaseHelper.executeDelete(
						GeocacheDataServiceConstants.CACHE_TABLE_NAME,
						createCacheEqualityWhereClause(cache));

				if (deleteSucceeded) {
					return cache;
				} else {
					return null;
				}
			}

			@Override
			protected void onPostExecute(Cache result) {
				super.onPostExecute(result);
				GeocacheCacheManagementRequestResponseListener.RequestStatus deleteRequestStatus = GeocacheCacheManagementRequestResponseListener.RequestStatus.SUCCESS;
				if (null == cache) {
					deleteRequestStatus = GeocacheCacheManagementRequestResponseListener.RequestStatus.FAIL;
				}

				geocacheCacheManagementRequestResponseListener
						.processGeocacheCacheManagementRequestResponse(
								deleteRequestStatus, cache);
			}
		}.execute();
	}

	private List<Cache> executeCacheWhereQuery(String whereClause,
			String orderBy) {
		List<Cache> caches = new ArrayList<Cache>();

		Cursor queryResults = geocacheDatabaseHelper.executeQuery(
				GeocacheDataServiceConstants.CACHE_TABLE_NAME,
				GeocacheDataServiceConstants.ALL_CACHE_COLUMNS, whereClause,
				orderBy);

		extractCachesFromQueryResults(caches, queryResults);

		if (null != queryResults) {
			queryResults.close();
		}

		return caches;

	}

	private void extractCachesFromQueryResults(List<Cache> cacheList,
			Cursor queryResults) {
		if (null != queryResults) {
			queryResults.moveToFirst();
			while (!queryResults.isAfterLast()) {
				cacheList.add(cursorToCache(queryResults));
				queryResults.moveToNext();
			}

			if (null != queryResults) {
				queryResults.close();
			}
		}
	}

	private ContentValues buildCacheContentValuesForInsert(Cache cache) {
		ContentValues values = new ContentValues();
		values.put(GeocacheDataServiceConstants.CACHE_COLUMN_NAME,
				cache.getName());
		values.put(GeocacheDataServiceConstants.CACHE_COLUMN_DESCRIPTION,
				cache.getDescription());
		values.put(GeocacheDataServiceConstants.CACHE_COLUMN_COORD_LAT, cache
				.getCacheCoordinate().getGeoPoint().getLatitudeE6());
		values.put(GeocacheDataServiceConstants.CACHE_COLUMN_COORD_LONG, cache
				.getCacheCoordinate().getGeoPoint().getLongitudeE6());
		values.put(GeocacheDataServiceConstants.CACHE_COLUMN_TYPE, cache
				.getCacheType().toString());
		return values;
	}

	private String createCacheEqualityWhereClause(Cache cache) {
		return GeocacheDataServiceConstants.CACHE_COLUMN_ID + "="
				+ cache.getID();
	}

	private Cache cursorToCache(Cursor cursor) {
		Cache cache = new Cache();
		cache.setID(cursor.getString(0));
		cache.setName(cursor.getString(1));
		cache.setDescription(cursor.getString(2));
		int latCoord = cursor.getInt(3);
		int longtCoord = cursor.getInt(4);
		cache.setCacheCoordinate(new CacheCoordinate(latCoord, longtCoord));
		cache.setCacheType(CacheType.valueOf(cursor.getString(5).toUpperCase().replaceAll("[^A-Z]", "_")));

		return cache;
	}

	private void setLocationLatLong(Location location,
			final CacheCoordinate cacheCoordinate) {
		location.setLatitude(cacheCoordinate.getGeoPoint().getLatitudeE6() / 1E6);
		location.setLongitude(cacheCoordinate.getGeoPoint().getLongitudeE6() / 1E6);
	}

	private static String quote(String value) {
		return GeocacheDataServiceConstants.QUOTE + value
				+ GeocacheDataServiceConstants.QUOTE;
	}

	private static String paren(String value) {
		return GeocacheDataServiceConstants.LPAREN + value
				+ GeocacheDataServiceConstants.RPAREN;
	}

	private static String implodeAndQuote(List<String> terms, String glue) {

		String output = "";

		if (terms.size() > 0) {
			StringBuilder sb = new StringBuilder();
			sb.append(quote(terms.get(0)));

			for (int i = 1; i < terms.size(); i++) {
				sb.append(glue);
				sb.append(quote(terms.get(i)));
			}

			output = sb.toString();
		}

		return output;
	}

	private class GeocacheDatabaseHelper extends SQLiteOpenHelper {
		private SQLiteDatabase readableDatabase = null;
		private SQLiteDatabase writeableDatabase = null;

		public GeocacheDatabaseHelper(Context context) {
			super(context, GeocacheDataServiceConstants.DATABASE_NAME, null,
					GeocacheDataServiceConstants.DATABASE_VERSION);
		}

		@Override
		public void onCreate(SQLiteDatabase database) {
			database.execSQL(GeocacheDataServiceConstants.CACHE_TABLE_CREATE);
			database.execSQL(GeocacheDataServiceConstants.USER_TABLE_CREATE);

			insertDefaultCaches(database);

		}

		@Override
		public void onUpgrade(SQLiteDatabase database, int oldVersion,
				int newVersion) {
			Log.wtf(GeocacheDatabaseHelper.class.getName(),
					"Upgrading database from version " + oldVersion + " to "
							+ newVersion + ", which will destroy all old data");
			database.execSQL("DROP TABLE IF EXISTS "
					+ GeocacheDataServiceConstants.USER_TABLE_NAME);
			database.execSQL("DROP TABLE IF EXISTS "
					+ GeocacheDataServiceConstants.CACHE_TABLE_NAME);
			onCreate(database);

		}

		@Override
		public SQLiteDatabase getReadableDatabase() {
			if (null == readableDatabase) {
				this.readableDatabase = super.getReadableDatabase();
			}

			return readableDatabase;
		}

		@Override
		public SQLiteDatabase getWritableDatabase() {
			if (null == writeableDatabase) {
				this.writeableDatabase = super.getWritableDatabase();
			}

			return writeableDatabase;
		}

		public Cursor executeQuery(String tableName, String[] columns,
				String whereClause, String orderBy) {
			Cursor cursor = null;
			try {
				cursor = geocacheDatabaseHelper.getReadableDatabase().query(
						tableName, columns, whereClause, null, null, null,
						orderBy, LIMIT_CLAUSE);
			} catch (SQLiteException exception) {
				Log.e(getClass().getName(), exception.toString());
			}

			return cursor;
		}

		public Boolean executeInsert(String tableName,
				ContentValues contentValues) {
			long insertID = INSERT_FAILED;
			try {
				insertID = geocacheDatabaseHelper.getWritableDatabase().insert(
						tableName, null, contentValues);
			} catch (SQLiteException exception) {
				Log.e(getClass().getName(), exception.toString());
			}

			return Boolean.valueOf(INSERT_FAILED != insertID);
		}

		public Boolean executeUpdate(String tableName,
				ContentValues contentValues, String whereClause) {
			int rowsAffected = 0;
			try {
				rowsAffected = geocacheDatabaseHelper.getWritableDatabase()
						.update(tableName, contentValues, whereClause, null);
			} catch (SQLiteException exception) {
				Log.e(getClass().getName(), exception.toString());
			}

			return Boolean.valueOf(0 != rowsAffected);
		}

		public Boolean executeDelete(String tableName, String whereClause) {
			int rowsAffected = 0;
			try {
				rowsAffected = geocacheDatabaseHelper.getWritableDatabase()
						.delete(tableName, whereClause, null);
			} catch (SQLiteException exception) {
				Log.e(getClass().getName(), exception.toString());
			}

			return Boolean.valueOf(0 != rowsAffected);
		}

		private void insertDefaultCaches(SQLiteDatabase database) {
			// insert test data // make some contacts
			ArrayList<Cache> caches = new ArrayList<Cache>();

			Cache c1 = new Cache();
			c1.setCacheCoordinate(new CacheCoordinate(44974580, -93232330));
			c1.setName("Superman's Shinning Light");
			c1.setDescription("The cache you seek is in a nice little spot in the heart of MPLS. Good Luck Hunting!");
			caches.add(c1);

			Cache c2 = new Cache();
			c2.setCacheCoordinate(new CacheCoordinate(44976590, -93224120));
			c2.setName("Holy Bottle");
			c2.setDescription("A bottle with hidden treasure.");
			caches.add(c2);

			Cache c3 = new Cache();
			c3.setCacheCoordinate(new CacheCoordinate(44972800, -93235350));
			c3.setName("Floating over Finland");
			c3.setDescription("Amazing what you can catch if you keep your eyes open.");
			caches.add(c3);

			for (Cache cache : caches) {
				ContentValues cacheContentValues = buildCacheContentValuesForInsert(cache);
				database.insert(GeocacheDataServiceConstants.CACHE_TABLE_NAME,
						null, cacheContentValues);
			}// end for (Contact contact : contacts) {
		}
	}
}